
/*-------------------------------------------------------------------------
 *
 * basebackup_to_shell.c
 *	  目标基础备份文件到一个 shell 命令
 *
 * Copyright (c) 2016-2022, PostgreSQL Global Development Group
 *
 *	  contrib/basebackup_to_shell/basebackup_to_shell.c
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/xact.h"
#include "backup/basebackup_target.h"
#include "miscadmin.h"
#include "storage/fd.h"
#include "utils/acl.h"
#include "utils/guc.h"

PG_MODULE_MAGIC;

typedef struct bbsink_shell
{
	/* 所有类型的接收器的通用信息。 */
	bbsink		base;

	/* 用户提供的目标详细字符串。 */
	char	   *target_detail;

	/* 当前用于备份的 Shell 命令模式。 */
	char	   *shell_command;

	/* 当前正在运行的命令。 */
	char	   *current_command;

	/* 管道到正在运行的命令。 */
	FILE	   *pipe;
} bbsink_shell;

void		_PG_init(void);

static void *fc_shell_check_detail(char *fc_target, char *fc_target_detail);
static bbsink *fc_shell_get_sink(bbsink *fc_next_sink, void *fc_detail_arg);

static void fc_bbsink_shell_begin_archive(bbsink *fc_sink,
									   const char *fc_archive_name);
static void fc_bbsink_shell_archive_contents(bbsink *fc_sink, size_t fc_len);
static void fc_bbsink_shell_end_archive(bbsink *fc_sink);
static void fc_bbsink_shell_begin_manifest(bbsink *fc_sink);
static void fc_bbsink_shell_manifest_contents(bbsink *fc_sink, size_t fc_len);
static void fc_bbsink_shell_end_manifest(bbsink *fc_sink);

static const bbsink_ops bbsink_shell_ops = {
	.begin_backup = bbsink_forward_begin_backup,
	.begin_archive = fc_bbsink_shell_begin_archive,
	.archive_contents = fc_bbsink_shell_archive_contents,
	.end_archive = fc_bbsink_shell_end_archive,
	.begin_manifest = fc_bbsink_shell_begin_manifest,
	.manifest_contents = fc_bbsink_shell_manifest_contents,
	.end_manifest = fc_bbsink_shell_end_manifest,
	.end_backup = bbsink_forward_end_backup,
	.cleanup = bbsink_forward_cleanup
};

static char *shell_command = "";
static char *shell_required_role = "";

void _PG_init(void)
{
	DefineCustomStringVariable("basebackup_to_shell.command",
							   "Shell command to be executed for each backup file.",
							   NULL,
							   &shell_command,
							   "",
							   PGC_SIGHUP,
							   0,
							   NULL, NULL, NULL);

	DefineCustomStringVariable("basebackup_to_shell.required_role",
							   "Backup user must be a member of this role to use shell backup target.",
							   NULL,
							   &shell_required_role,
							   "",
							   PGC_SIGHUP,
							   0,
							   NULL, NULL, NULL);

	MarkGUCPrefixReserved("basebackup_to_shell");

	BaseBackupAddTarget("shell", fc_shell_check_detail, fc_shell_get_sink);
}

/*
 * 我们选择将合理性检查推迟到 shell_get_sink()，因此
 * 只是在不执行任何操作的情况下传递目标详细信息。不过，我们在这里进行
 * 权限检查，在进行任何实际工作之前。
 */
static void * fc_shell_check_detail(char *fc_target, char *fc_target_detail)
{
	if (shell_required_role[0] != '\0')
	{
		Oid			fc_roleid;

		StartTransactionCommand();
		fc_roleid = get_role_oid(shell_required_role, true);
		if (!has_privs_of_role(GetUserId(), fc_roleid))
			ereport(ERROR,
					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
					 errmsg("permission denied to use basebackup_to_shell")));
		CommitTransactionCommand();
	}

	return fc_target_detail;
}

/*
 * 设置一个 bbsink 来实现这个基础备份目标。
 *
 * 这也是在 %d 存在时检查目标详细信息是否给定的合适地方。
 */
static bbsink * fc_shell_get_sink(bbsink *fc_next_sink, void *fc_detail_arg)
{
	bbsink_shell *fc_sink;
	bool		fc_has_detail_escape = false;
	char	   *fc_c;

	/*
	 * 设置 bbsink。
	 *
	 * 我们记住 basebackup_to_shell.shell_command 的当前值，以
	 * 确保在备份期间它不会发生改变。
	 */
	fc_sink = palloc0(sizeof(bbsink_shell));
	*((const bbsink_ops **) &fc_sink->base.bbs_ops) = &bbsink_shell_ops;
	fc_sink->base.bbs_next = fc_next_sink;
	fc_sink->target_detail = fc_detail_arg;
	fc_sink->shell_command = pstrdup(shell_command);

	/* 拒绝空的 shell 命令。 */
	if (fc_sink->shell_command[0] == '\0')
		ereport(ERROR,
				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				errmsg("shell command for backup is not configured"));

	/* 确定我们使用的 shell 命令是否包含 %d。 */
	for (fc_c = fc_sink->shell_command; *fc_c != '\0'; ++fc_c)
	{
		if (fc_c[0] == '%' && fc_c[1] != '\0')
		{
			if (fc_c[1] == 'd')
				fc_has_detail_escape = true;
			++fc_c;
		}
	}

	/* 如果使用了 %d，则应该有目标详细信息，否则不应有。 */
	if (fc_has_detail_escape && fc_sink->target_detail == NULL)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("a target detail is required because the configured command includes %%d"),
				 errhint("Try \"pg_basebackup --target shell:DETAIL ...\"")));
	else if (!fc_has_detail_escape && fc_sink->target_detail != NULL)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("a target detail is not permitted because the configured command does not include %%d")));

	/*
	 * 由于我们将用户提供的字符串传递给 popen()，它将
	 * 被 shell 解释，这是一种潜在的安全
	 * 漏洞，因为触发此模块的用户不一定是
	 * 超级用户。为了避免麻烦，我们必须禁止任何 shell
	 * 元字符；为了保持保守并简化问题，我们
	 * 只允许字母数字字符。
	 */
	if (fc_sink->target_detail != NULL)
	{
		char	   *fc_d;
		bool		fc_scary = false;

		for (fc_d = fc_sink->target_detail; *fc_d != '\0'; ++fc_d)
		{
			if (*fc_d >= 'a' && *fc_d <= 'z')
				continue;
			if (*fc_d >= 'A' && *fc_d <= 'Z')
				continue;
			if (*fc_d >= '0' && *fc_d <= '9')
				continue;
			fc_scary = true;
			break;
		}

		if (fc_scary)
			ereport(ERROR,
					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					errmsg("target detail must contain only alphanumeric characters"));
	}

	return &fc_sink->base;
}

/*
 * 构造我们实际要运行的确切 shell 命令，
 * 并根据转义序列进行适当的替换。
 */
static char * fc_shell_construct_command(char *fc_base_command, const char *fc_filename,
						char *fc_target_detail)
{
	StringInfoData fc_buf;
	char	   *fc_c;

	initStringInfo(&fc_buf);
	for (fc_c = fc_base_command; *fc_c != '\0'; ++fc_c)
	{
		/* 除 '%' 以外的任何内容都逐字复制。 */
		if (*fc_c != '%')
		{
			appendStringInfoChar(&fc_buf, *fc_c);
			continue;
		}

		/* 每当我们看到 '%' 时，我们也会消耗下一个字符。 */
		++fc_c;

		/*
		 * 下一个字符决定我们在这里插入什么，或者可能
		 * 使我们抛出错误。
		 */
		if (*fc_c == '%')
		{
			/* '%%' 替换为一个 '%' */
			appendStringInfoChar(&fc_buf, '%');
		}
		else if (*fc_c == 'f')
		{
			/* '%f' 被替换为文件名 */
			appendStringInfoString(&fc_buf, fc_filename);
		}
		else if (*fc_c == 'd')
		{
			/* '%d' 被替换为目标详细信息 */
			appendStringInfoString(&fc_buf, fc_target_detail);
		}
		else if (*fc_c == '\0')
		{
			/* 不完整的转义序列，预期后面有一个字符 */
			ereport(ERROR,
					errcode(ERRCODE_SYNTAX_ERROR),
					errmsg("shell command ends unexpectedly after escape character \"%%\""));
		}
		else
		{
			/* 未知的转义序列 */
			ereport(ERROR,
					errcode(ERRCODE_SYNTAX_ERROR),
					errmsg("shell command contains unexpected escape sequence \"%c\"",
						   *fc_c));
		}
	}

	return fc_buf.data;
}

/*
 * 在所有数据写入完成后结束执行 shell 命令。
 */
static void fc_shell_finish_command(bbsink_shell *fc_sink)
{
	int			fc_pclose_rc;

	/* 应该有一个命令在运行。 */
	Assert(fc_sink->current_command != NULL);
	Assert(fc_sink->pipe != NULL);

	/* 关闭我们打开的管道。 */
	fc_pclose_rc = ClosePipeStream(fc_sink->pipe);
	if (fc_pclose_rc == -1)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close pipe to external command: %m")));
	else if (fc_pclose_rc != 0)
	{
		ereport(ERROR,
				(errcode(ERRCODE_EXTERNAL_ROUTINE_EXCEPTION),
				 errmsg("shell command \"%s\" failed",
						fc_sink->current_command),
				 errdetail_internal("%s", wait_result_to_str(fc_pclose_rc))));
	}

	/* 清理。 */
	fc_sink->pipe = NULL;
	pfree(fc_sink->current_command);
	fc_sink->current_command = NULL;
}

/*
 * 启动 shell 命令，将 %f 替换为当前文件名。
 */
static void fc_shell_run_command(bbsink_shell *fc_sink, const char *fc_filename)
{
	/* 不应该有其他正在运行的任务。 */
	Assert(fc_sink->current_command == NULL);
	Assert(fc_sink->pipe == NULL);

	/* 构造一个合适的命令。 */
	fc_sink->current_command = fc_shell_construct_command(fc_sink->shell_command,
													fc_filename,
													fc_sink->target_detail);

	/* 运行它。 */
	fc_sink->pipe = OpenPipeStream(fc_sink->current_command, PG_BINARY_W);
	if (fc_sink->pipe == NULL)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not execute command \"%s\": %m",
						fc_sink->current_command)));
}

/*
 * 将累积的数据发送到正在运行的 shell 命令。
 */
static void fc_shell_send_data(bbsink_shell *fc_sink, size_t fc_len)
{
	/* 应该有一个命令在运行。 */
	Assert(fc_sink->current_command != NULL);
	Assert(fc_sink->pipe != NULL);

	/* 尝试写入数据。 */
	if (fwrite(fc_sink->base.bbs_buffer, fc_len, 1, fc_sink->pipe) != 1 ||
		ferror(fc_sink->pipe))
	{
		if (errno == EPIPE)
		{
			/*
			 * 我们即将抛出的错误无论如何会关闭命令，
			 * 但通过这样做我们可能会得到更多有意义的错误信息。
			 * 如果没有，我们将落入下面的通用错误。
			 */
			fc_shell_finish_command(fc_sink);
			errno = EPIPE;
		}
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write to shell backup program: %m")));
	}
}

/*
 * 在归档开始时，启动 shell 命令并转发到下一个水槽。
 */
static void fc_bbsink_shell_begin_archive(bbsink *fc_sink, const char *fc_archive_name)
{
	bbsink_shell *fc_mysink = (bbsink_shell *) fc_sink;

	fc_shell_run_command(fc_mysink, fc_archive_name);
	bbsink_forward_begin_archive(fc_sink, fc_archive_name);
}

/*
 * 将归档内容发送到命令的 stdin 并转发到下一个水槽。
 */
static void fc_bbsink_shell_archive_contents(bbsink *fc_sink, size_t fc_len)
{
	bbsink_shell *fc_mysink = (bbsink_shell *) fc_sink;

	fc_shell_send_data(fc_mysink, fc_len);
	bbsink_forward_archive_contents(fc_sink, fc_len);
}

/*
 * 在归档结束时，关闭shell命令并转发到下一个接收器。
 */
static void fc_bbsink_shell_end_archive(bbsink *fc_sink)
{
	bbsink_shell *fc_mysink = (bbsink_shell *) fc_sink;

	fc_shell_finish_command(fc_mysink);
	bbsink_forward_end_archive(fc_sink);
}

/*
 * 在清单开始时，启动shell命令并转发到下一个接收器。
 */
static void fc_bbsink_shell_begin_manifest(bbsink *fc_sink)
{
	bbsink_shell *fc_mysink = (bbsink_shell *) fc_sink;

	fc_shell_run_command(fc_mysink, "backup_manifest");
	bbsink_forward_begin_manifest(fc_sink);
}

/*
 * 将清单内容发送到命令的标准输入并转发到下一个接收器。
 */
static void fc_bbsink_shell_manifest_contents(bbsink *fc_sink, size_t fc_len)
{
	bbsink_shell *fc_mysink = (bbsink_shell *) fc_sink;

	fc_shell_send_data(fc_mysink, fc_len);
	bbsink_forward_manifest_contents(fc_sink, fc_len);
}

/*
 * 在清单结束时，关闭shell命令并转发到下一个接收器。
 */
static void fc_bbsink_shell_end_manifest(bbsink *fc_sink)
{
	bbsink_shell *fc_mysink = (bbsink_shell *) fc_sink;

	fc_shell_finish_command(fc_mysink);
	bbsink_forward_end_manifest(fc_sink);
}
